program PerPixelLighting;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  SampleCommon,
  DIrr in '..\..\DSource\DIrr.pas',
  DIrr.GUI in '..\..\DSource\DIrr.GUI.pas',
  DIrr.Color in '..\..\DSource\DIrr.Color.pas',
  DIrr.Rect in '..\..\DSource\DIrr.Rect.pas',
  DIrr.Event in '..\..\DSource\DIrr.Event.pas',
  DIrr.Vector2D in '..\..\DSource\DIrr.Vector2D.pas',
  DIrr.Vector3D in '..\..\DSource\DIrr.Vector3D.pas',
  DIrr.Video in '..\..\DSource\DIrr.Video.pas',
  DIrr.Base in '..\..\DSource\DIrr.Base.pas',
  DIrr.Types in '..\..\DSource\DIrr.Types.pas',
  DIrr.Scene in '..\..\DSource\DIrr.Scene.pas',
  DIrr.Structs in '..\..\DSource\DIrr.Structs.pas',
  DIrr.Aabbox3D in '..\..\DSource\DIrr.Aabbox3D.pas',
  DIrr.Matrix4 in '..\..\DSource\DIrr.Matrix4.pas',
  DIrr.Texture in '..\..\DSource\DIrr.Texture.pas',
  DIrr.Collections in '..\..\DSource\DIrr.Collections.pas',
  DIrr.Particle in '..\..\DSource\DIrr.Particle.pas',
  DIrr.Dimension2d in '..\..\DSource\DIrr.Dimension2d.pas',
  DIrr.Material in '..\..\DSource\DIrr.Material.pas',
  DIrr.RenderServices in '..\..\DSource\DIrr.RenderServices.pas',
  DIrr.Consts in '..\..\DSource\DIrr.Consts.pas';

type
  TMyEventReceiver = class(TEventReceiver)
  private
    FProblemText: TGUIStaticText;
    FListBox: TGUIListBox;
    FRoom: TSceneNode;
    FEarth: TSceneNode;
    FDriver: TVideoDriver;
  private
    procedure SetMaterial;
  public
    procedure Init(ARoom, AnEarth: TSceneNode; AnEnv: TGUIEnvironment; Driver: TVideoDriver);
  public
    function OnEvent(const Event: SEvent): Boolean; override;
  end;

var
  DriverType: E_DRIVER_TYPE;
  Device: TIrrlichtDevice;
  Driver: TVideoDriver;
  Smgr: TSceneManager;
  Env: TGUIEnvironment;
  Camera: TCameraSceneNode;
  RoomMesh: TAnimatedMesh;
  Room: TSceneNode = nil;
  Earth: TSceneNode = nil;
  NormalMap: TTexture;
  TangentMesh: TMesh;
  EarthMesh: TAnimatedMesh;
  Manipulator: TMeshManipulator;
  TangentSphereMesh: TMesh;
  m: TMatrix4F;
  EarthNormalMap: TTexture;
  Anim: TSceneNodeAnimator;
  Light1: TLightSceneNode;
  Bill: TBillboardSceneNode;
  Light2: TSceneNode;
  ps: TParticleSystemSceneNode;
  EParam: SEmitterParams;
  em: TParticleEmitter;
  Paf: TParticleAffector;
  Receiver: TMyEventReceiver;
  LastFPS: Integer = -1;
  FPS: Integer;
  MaterialPtr: PMaterial;
  MeshManipulator: TMeshManipulator;
  Mesh: TMesh;
  Str: String;
{ TMyEventReceiver }

procedure TMyEventReceiver.Init(ARoom, AnEarth: TSceneNode;
  AnEnv: TGUIEnvironment; Driver: TVideoDriver);
var
  Skin: TGUISkin;
  Font: TGUIFont;
  Window: TGUIWindow;
  Renderer: TMaterialRenderer;
begin
  // store pointer to room so we can change its drawing mode
  FRoom := ARoom;
  FEarth := AnEarth;
  FDriver := Driver;

  // set a nicer font
  Skin := Env.GetSkin;
  Font := Env.GetFont('..\..\media\fonthaettenschweiler.bmp');
  if Assigned(Font) then
    Skin.SetFont(Font);

  // add window and listbox
  Window := Env.AddWindow(IrrRect(460, 375, 630, 470), False, 'Use ''E'' + ''R'' to change');

  FListBox := Env.AddListBox(IrrRect(2, 22, 165, 88), Window);

  FListBox.AddItem('Diffuse');
  FListBox.AddItem('Bump mapping');
  FListBox.AddItem('Parallax mapping');
  FListBox.SetSelected(1);

  // create problem text
  FProblemText := Env.AddStaticText(
   'Your harware or this renderer is not able to use the needed shaders for this material. Using fall back materilas.',
   IrrRect(150, 20, 470, 80)
   );
  FProblemText.SetOverrideColor(IrrColor(100, 255, 255, 255));

  // set start material (prefer parallax mapping if available)
  Renderer := Driver.GetMaterialRenderer(Ord(EMT_PARALLAX_MAP_SOLID));
  if Assigned(Renderer) and (Renderer.GetRenderCapability = 0) then
    FListBox.SetSelected(2);

  // set the material which is selected in the listbox
  SetMaterial;
end;

function TMyEventReceiver.OnEvent(const Event: SEvent): Boolean;
var
  sel: Integer;
begin
  Result := False;

  // check if user presses the key 'E' or 'R'
  if (Event.EventType = EET_KEY_INPUT_EVENT) and
    (not Event.KeyInput.PressedDown) and Assigned(FRoom) and Assigned(FListBox) then
  begin
    // change selected item in listbox
    sel := FListBox.GetSelected;
    if Event.KeyInput.Key = KEY_KEY_R then
      Inc(sel)
    else if Event.KeyInput.Key = KEY_KEY_E then
      Dec(sel)
    else
      Exit;

    if sel > 2 then sel := 0;
    if sel < 0 then sel := 2;
    FListBox.SetSelected(sel);

    // set the material which is selected in the listbox
    SetMaterial;
  end;
end;

procedure TMyEventReceiver.SetMaterial;
var
  mtype: E_MATERIAL_TYPE;
  Renderer: TMaterialRenderer;
begin
  mtype := EMT_SOLID;

  // change material setting
  case FListBox.GetSelected of
    0: mtype := EMT_SOLID;
    1: mtype := EMT_NORMAL_MAP_SOLID;
    2: mtype := EMT_PARALLAX_MAP_SOLID;
  end;

  FRoom.SetMaterialType(mtype);

  // change material setting
  case FListBox.GetSelected of
    0: mtype := EMT_TRANSPARENT_VERTEX_ALPHA;
    1: mtype := EMT_NORMAL_MAP_TRANSPARENT_VERTEX_ALPHA;
    2: mtype := EMT_PARALLAX_MAP_TRANSPARENT_VERTEX_ALPHA;
  end;

  FEarth.SetMaterialType(mtype);

  (*
  We need to add a warning if the materials will not be able to
  be displayed 100% correctly. This is no problem, they will be
  rendered using fall back materials, but at least the user
  should know that it would look better on better hardware. We
  simply check if the material renderer is able to draw at full
  quality on the current hardware. The
  IMaterialRenderer::getRenderCapability() returns 0 if this is
  the case.
  *)
  Renderer := Driver.GetMaterialRenderer(Ord(mtype));

  // display some problem text when problem
  if (not Assigned(Renderer)) or (Renderer.GetRenderCapability <> 0) then
    FProblemText.SetVisible(True)
  else
    FProblemText.SetVisible(False);
end;

begin
  try
    { TODO -oUser -cConsole Main : Insert code here }
    DriverType := DriverChoiceConsole;
    if DriverType = EDT_COUNT then
    begin
      ExitCode := 1;
      Exit;
    end;

    // create device and exit if creation failed
    Device := createDevice(DriverType, [640, 480]);
    if Device = nil then
    begin
      ExitCode := 1;   // could not create selected driver.
      Exit;
    end;

    (*
    Before we start with the interesting stuff, we do some simple things:
    Store pointers to the most important parts of the engine (video driver,
    scene manager, gui environment) to safe us from typing too much, add an
    irrlicht engine logo to the window and a user controlled first person
    shooter style camera. Also, we let the engine know that it should store
    all textures in 32 bit. This necessary because for parallax mapping, we
    need 32 bit textures.
    *)
    Driver := Device.GetVideoDriver;
    Smgr := Device.GetSceneManager;
    Env := Device.GetGUIEnvironment;

    Driver.SetTextureCreationFlag(ETCF_ALWAYS_32_BIT, True);

    // add irrlicht logo
    Env.AddImage(Driver.GetTexture('..\..\media\irrlichtlogo3.png'), Vector2D(10, 10));

    // add camera
    Camera := Smgr.AddCameraSceneNodeFPS();
    Camera.SetPosition(Vector3DF(-200, 200, -200));

    // disable mouse cursor
    Device.GetCursorControl.SetVisible(False);

    (*
    Because we want the whole scene to look a little bit scarier, we add
    some fog to it. This is done by a call to IVideoDriver::setFog(). There
    you can set various fog settings. In this example, we use pixel fog,
    because it will work well with the materials we'll use in this example.
    Please note that you will have to set the material flag EMF_FOG_ENABLE
    to 'true' in every scene node which should be affected by this fog.
    *)
    Driver.SetFog(IrrColor(138, 125, 81, 0), EFT_FOG_LINEAR, 250, 1000, 0.003, True, False);

    (*
    To be able to display something interesting, we load a mesh from a .3ds
    file which is a room I modeled with anim8or. It is the same room as
    from the specialFX example. Maybe you remember from that tutorial, I am
    no good modeler at all and so I totally messed up the texture mapping
    in this model, but we can simply repair it with the
    IMeshManipulator::makePlanarTextureMapping() method.
    *)

    RoomMesh := Smgr.GetMesh('..\..\media\room.3ds');

    if Assigned(RoomMesh) then
    begin
      // The Room mesh doesn't have proper Texture Mapping on the
      // floor, so we can recreate them on runtime
      MeshManipulator := Smgr.GetMeshManipulator;
      Mesh := RoomMesh.GetMesh(0);

      MeshManipulator.MakePlanarTextureMapping(Mesh, 0.003);

      (*
      Now for the first exciting thing: If we successfully loaded the
      mesh we need to apply textures to it. Because we want this room
      to be displayed with a very cool material, we have to do a
      little bit more than just set the textures. Instead of only
      loading a color map as usual, we also load a height map which
      is simply a grayscale texture. From this height map, we create
      a normal map which we will set as second texture of the room.
      If you already have a normal map, you could directly set it,
      but I simply didn't find a nice normal map for this texture.
      The normal map texture is being generated by the
      makeNormalMapTexture method of the VideoDriver. The second
      parameter specifies the height of the heightmap. If you set it
      to a bigger value, the map will look more rocky.
      *)
      NormalMap := Driver.GetTexture('..\..\media\rockwall_height.bmp');

      if Assigned(NormalMap) then
        Driver.MakeNormalMapTexture(NormalMap, 9.0);

  (*
      // The Normal Map and the displacement map/height map in the alpha channel
      video::ITexture* normalMap =
        driver->getTexture("../../media/rockwall_NRM.tga");
  *)
      (*
      But just setting color and normal map is not everything. The
      material we want to use needs some additional informations per
      vertex like tangents and binormals. Because we are too lazy to
      calculate that information now, we let Irrlicht do this for us.
      That's why we call IMeshManipulator::createMeshWithTangents().
      It creates a mesh copy with tangents and binormals from another
      mesh. After we've done that, we simply create a standard
      mesh scene node with this mesh copy, set color and normal map
      and adjust some other material settings. Note that we set
      EMF_FOG_ENABLE to true to enable fog in the room.
      *)

      TangentMesh := MeshManipulator.CreateMeshWithTangents(Mesh);

      Room := Smgr.AddMeshSceneNode(TangentMesh);
      Room.SetMaterialTexture(0, Driver.GetTexture('..\..\media\rockwall.jpg'));
      Room.SetMaterialTexture(1, NormalMap);

      // Stones don't glitter..
      MaterialPtr := Room.GetMaterial(0);
      MaterialPtr^.FSpecularColor.Color32 := 0;
      MaterialPtr^.FShininess := 0.0;

      Room.SetMaterialFlag(EMF_FOG_ENABLE, True);
      Room.SetMaterialType(EMT_PARALLAX_MAP_SOLID);
      // adjust height for parallax effect
      MaterialPtr^.FMaterialTypeParam := 1.0 / 64.0;

      // drop mesh because we created it with a create.. call.
      TangentMesh.Drop;
    end;

    (*
    After we've created a room shaded by per pixel lighting, we add a
    sphere into it with the same material, but we'll make it transparent.
    In addition, because the sphere looks somehow like a familiar planet,
    we make it rotate. The procedure is similar as before. The difference
    is that we are loading the mesh from an .x file which already contains
    a color map so we do not need to load it manually. But the sphere is a
    little bit too small for our needs, so we scale it by the factor 50.
    *)

    // add earth sphere
    EarthMesh := Smgr.GetMesh('..\..\media\earth.x');
    if Assigned(EarthMesh) then
    begin
      //perform various task with the mesh manipulator
      Manipulator := Smgr.GetMeshManipulator;

      // create mesh copy with tangent informations from original earth.x mesh
      TangentSphereMesh := Manipulator.CreateMeshWithTangents(EarthMesh.GetMesh(0));

      // set the alpha value of all vertices to 200
      //>>> Manipulator.SetVertexColorAlpha(TangentSphereMesh, 200);

      // scale the mesh by factor 50
      m.Init;
      m.SetScale(Vector3DF(50, 50, 50));
      Manipulator.Transform(TangentSphereMesh, m);

      Earth := Smgr.AddMeshSceneNode(TangentSphereMesh);

      Earth.SetPosition(Vector3DF(-70, 130, 45));

      // load heightmap, create normal map from it and set it
      EarthNormalMap := Driver.GetTexture('..\..\media\earthbump.jpg');
      if Assigned(EarthNormalMap) then
      begin
        Driver.MakeNormalMapTexture(EarthNormalMap, 20);
        Earth.SetMaterialTexture(1, EarthNormalMap);
        Earth.SetMaterialType(EMT_NORMAL_MAP_TRANSPARENT_VERTEX_ALPHA);
      end;

      // adjust material settings
      Earth.SetMaterialFlag(EMF_FOG_ENABLE, True);

      // add rotation animator
      Anim := Smgr.CreateRotationAnimator(Vector3DF(0, 0.1, 0));
      Earth.AddAnimator(Anim);
      Anim.Drop;

      // drop mesh because we created it with a create.. call.
      TangentSphereMesh.Drop;
    end;

    (*
    Per pixel lighted materials only look cool when there are moving
    lights. So we add some. And because moving lights alone are so boring,
    we add billboards to them, and a whole particle system to one of them.
    We start with the first light which is red and has only the billboard
    attached.
    *)

    // add light 1 (more green)
    Light1 := Smgr.AddLightSceneNode(nil, Vector3DF(0, 0, 0), IrrColorF(0.5, 1.0, 0.5, 0.0), 800);

    Light1.SetDebugDataVisible([EDS_BBOX]);

    // add fly circle animator to light 1
    Anim := Smgr.CreateFlyCircleAnimator(Vector3DF(50, 300, 0), 190, -0.003);
    Light1.AddAnimator(Anim);
    Anim.Drop;

    // attach billboard to the light
    Bill := Smgr.AddBillboardSceneNode(Light1, Dimension2DF(60, 60));

    Bill.SetMaterialFlag(EMF_LIGHTING, False);
    Bill.SetMaterialFlag(EMF_ZWRITE_ENABLE, False);
    Bill.SetMaterialType(EMT_TRANSPARENT_ADD_COLOR);
    Bill.SetMaterialTexture(0, Driver.GetTexture('..\..\media\particlegreen.jpg'));

    (*
    Now the same again, with the second light. The difference is that we
    add a particle system to it too. And because the light moves, the
    particles of the particlesystem will follow. If you want to know more
    about how particle systems are created in Irrlicht, take a look at the
    specialFx example. Maybe you will have noticed that we only add 2
    lights, this has a simple reason: The low end version of this material
    was written in ps1.1 and vs1.1, which doesn't allow more lights. You
    could add a third light to the scene, but it won't be used to shade the
    walls. But of course, this will change in future versions of Irrlicht
    where higher versions of pixel/vertex shaders will be implemented too.
    *)

    // add light 2 (red)
    Light2 := Smgr.AddLightSceneNode(nil, Vector3DF(0, 0, 0),
      IrrColorF(1.0, 0.2, 0.2, 0.0), 800);

    // add fly circle animator to light 2
    Anim := Smgr.CreateFlyCircleAnimator(Vector3DF(0, 150, 0), 200,
      0.001, Vector3DF(0.2, 0.9, 0.0));
    Light2.AddAnimator(Anim);
    Anim.Drop;

    // attach billboard to light
    Bill := Smgr.AddBillboardSceneNode(Light2, Dimension2DF(120, 120));
    Bill.SetMaterialFlag(EMF_LIGHTING, False);
    Bill.SetMaterialFlag(EMF_ZWRITE_ENABLE, False);
    Bill.SetMaterialType(EMT_TRANSPARENT_ADD_COLOR);
    Bill.SetMaterialTexture(0, Driver.GetTexture('..\..\media\particlered.bmp'));

    // add particle system
    ps := Smgr.AddParticleSystemSceneNode(False, Light2);

    // create and set emitter
    EParam.InitDefault;
    EParam.MinParticlesPerSecond := 80;
    EParam.MaxParticlesPerSecond := 100;
    EParam.MinStartColor := IrrColor(255, 255, 255, 10);
    EParam.MaxStartColor := IrrColor(255, 255, 255, 10);
    EParam.LifeTimeMin := 400;
    EParam.LifeTimeMax := 1100;

    em := ps.CreateBoxEmitter(AabBox3DF(-3, 0, -3, 3, 1, 3), EParam);

    em.SetMinStartSize(Dimension2DF(30, 40));
    em.SetMaxStartSize(Dimension2DF(30, 40));

    ps.SetEmitter(em);
    em.Drop;

    // create and set affector
    Paf := ps.CreateFadeOutParticleAffector;
    ps.AddAffector(Paf);
    Paf.Drop;

    // adjust some material settings
    ps.SetMaterialFlag(EMF_LIGHTING, False);
    ps.SetMaterialFlag(EMF_ZWRITE_ENABLE, False);
    ps.SetMaterialTexture(0, Driver.GetTexture('..\..\media\fireball.bmp'));
    ps.SetMaterialType(EMT_TRANSPARENT_ADD_COLOR);

    Receiver := TMyEventReceiver.Create;
    Receiver.Init(Room, Earth, Env, Driver);

    Device.SetEventReceiver(Receiver);

    (*
    Finally, draw everything. That's it.
    *)

    LastFPS := -1;

    while (Device.Run and Assigned(Driver)) do
    begin
      if Device.IsWindowActive then
      begin
        Driver.BeginScene(True, True, IrrColor(0, 0, 0, 0));

        Smgr.DrawAll;
        Env.DrawAll;

        Driver.EndScene;

        FPS := Driver.GetFPS;
        if LastFPS <> FPS then
        begin
          Str := 'Per pixel lighting example - Irrlicht Engine [';
          Str := Str + String(Driver.GetName) + '] FPS:';
          Str := Str + IntToStr(FPS);

          Device.SetWindowCaption(PChar(Str));
          LastFPS := FPS;
        end;
      end;
    end;

    Device.Drop;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
